From e003d24c320c7cc0a24d04a19a6492870fe0420e Mon Sep 17 00:00:00 2001
From: Jerome Brunet <jbrunet@baylibre.com>
Date: Thu, 30 Mar 2017 12:17:27 +0200
Subject: [PATCH] ASoC: meson: add initial i2s dai support

Add support for the i2s dai found on Amlogic Meson SoC family.
With this initial implementation, only playback is supported.
Capture will be part of furture work.

Signed-off-by: Jerome Brunet <jbrunet@baylibre.com>
Signed-off-by: Neil Armstrong <narmstrong@baylibre.com>
---
 sound/soc/meson/Kconfig   |   2 +-
 sound/soc/meson/Makefile  |   2 +
 sound/soc/meson/i2s-dai.c | 465 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 468 insertions(+), 1 deletion(-)
 create mode 100644 sound/soc/meson/i2s-dai.c

diff --git a/sound/soc/meson/Kconfig b/sound/soc/meson/Kconfig
index ad31a11..604c9e2 100644
--- a/sound/soc/meson/Kconfig
+++ b/sound/soc/meson/Kconfig
@@ -12,5 +12,5 @@ config SND_SOC_MESON_I2S
 	tristate "Meson i2s interface"
 	depends on SND_SOC_MESON
 	help
-	  Say Y or M if you want to add support for i2s dma driver for Amlogic
+	  Say Y or M if you want to add support for i2s driver for Amlogic
 	  Meson SoCs.
diff --git a/sound/soc/meson/Makefile b/sound/soc/meson/Makefile
index 273f275..ea06dde 100644
--- a/sound/soc/meson/Makefile
+++ b/sound/soc/meson/Makefile
@@ -1,5 +1,7 @@
 snd-soc-meson-audio-core-objs	 := audio-core.o
 snd-soc-meson-aiu-i2s-dma-objs	 := aiu-i2s-dma.o
+snd-soc-meson-i2s-dai-objs	 := i2s-dai.o
 
 obj-$(CONFIG_SND_SOC_MESON)		+= snd-soc-meson-audio-core.o
 obj-$(CONFIG_SND_SOC_MESON_I2S)		+= snd-soc-meson-aiu-i2s-dma.o
+obj-$(CONFIG_SND_SOC_MESON_I2S)		+= snd-soc-meson-i2s-dai.o
diff --git a/sound/soc/meson/i2s-dai.c b/sound/soc/meson/i2s-dai.c
new file mode 100644
index 0000000..1008af8
--- /dev/null
+++ b/sound/soc/meson/i2s-dai.c
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2017 BayLibre, SAS
+ * Author: Jerome Brunet <jbrunet@baylibre.com>
+ * Copyright (C) 2017 Amlogic, Inc. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <linux/clk.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "aiu-regs.h"
+#include "audio-core.h"
+
+#define DRV_NAME "meson-i2s-dai"
+
+struct meson_i2s_dai {
+	struct meson_audio_core_data *core;
+	struct clk *mclk;
+	struct clk *bclks;
+	struct clk *iface;
+	struct clk *fast;
+	bool bclks_idle;
+};
+
+#define AIU_CLK_CTRL_I2S_DIV_EN			BIT(0)
+#define AIU_CLK_CTRL_I2S_DIV_MASK		GENMASK(3, 2)
+#define AIU_CLK_CTRL_AOCLK_POLARITY_MASK	BIT(6)
+#define AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL	(0 << 6)
+#define AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED	(1 << 6)
+#define AIU_CLK_CTRL_ALRCLK_POLARITY_MASK	BIT(7)
+#define AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL	(0 << 7)
+#define AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED	(1 << 7)
+#define AIU_CLK_CTRL_ALRCLK_SKEW_MASK		GENMASK(9, 8)
+#define AIU_CLK_CTRL_ALRCLK_LEFT_J		(0 << 8)
+#define AIU_CLK_CTRL_ALRCLK_I2S			(1 << 8)
+#define AIU_CLK_CTRL_ALRCLK_RIGHT_J		(2 << 8)
+#define AIU_CLK_CTRL_MORE_I2S_DIV_MASK		GENMASK(5, 0)
+#define AIU_CLK_CTRL_MORE_I2S_DIV(div)		(((div) - 1) << 0)
+#define AIU_CODEC_DAC_LRCLK_CTRL_DIV_MASK	GENMASK(11, 0)
+#define AIU_CODEC_DAC_LRCLK_CTRL_DIV(div)	(((div) - 1) << 0)
+#define AIU_I2S_DAC_CFG_PAYLOAD_SIZE_MASK	GENMASK(1, 0)
+#define AIU_I2S_DAC_CFG_AOCLK_32		(0 << 0)
+#define AIU_I2S_DAC_CFG_AOCLK_48		(2 << 0)
+#define AIU_I2S_DAC_CFG_AOCLK_64		(3 << 0)
+#define AIU_I2S_MISC_HOLD_EN			BIT(2)
+#define AIU_I2S_SOURCE_DESC_MODE_8CH		BIT(0)
+#define AIU_I2S_SOURCE_DESC_MODE_24BIT		BIT(5)
+#define AIU_I2S_SOURCE_DESC_MODE_32BIT		BIT(9)
+#define AIU_I2S_SOURCE_DESC_MODE_SPLIT		BIT(11)
+
+static void __hold(struct meson_i2s_dai *priv, bool enable)
+{
+	regmap_update_bits(priv->core->aiu, AIU_I2S_MISC,
+			   AIU_I2S_MISC_HOLD_EN,
+			   enable ? AIU_I2S_MISC_HOLD_EN : 0);
+}
+
+static void __divider_enable(struct meson_i2s_dai *priv, bool enable)
+{
+	regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL,
+			   AIU_CLK_CTRL_I2S_DIV_EN,
+			   enable ? AIU_CLK_CTRL_I2S_DIV_EN : 0);
+}
+
+static void __playback_start(struct meson_i2s_dai *priv)
+{
+	__divider_enable(priv, true);
+	__hold(priv, false);
+}
+
+static void __playback_stop(struct meson_i2s_dai *priv, bool clk_force)
+{
+	__hold(priv, true);
+	/* Disable the bit clks if necessary */
+	if (clk_force || !priv->bclks_idle)
+		__divider_enable(priv, false);
+}
+
+static int meson_i2s_dai_trigger(struct snd_pcm_substream *substream, int cmd,
+				 struct snd_soc_dai *dai)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+	bool clk_force_stop = false;
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_RESUME:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+		__playback_start(priv);
+		return 0;
+
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+		clk_force_stop = true;
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+		__playback_stop(priv, clk_force_stop);
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int __bclks_set_rate(struct meson_i2s_dai *priv, unsigned int srate,
+			    unsigned int width)
+{
+	unsigned int fs;
+
+	/* Get the oversampling factor */
+	fs = DIV_ROUND_CLOSEST(clk_get_rate(priv->mclk), srate);
+
+	/*
+	 * This DAI is usually connected to the dw-hdmi which does not support
+	 * bclk being 32 * lrclk or 48 * lrclk
+	 * Restrict to blck = 64 * lrclk
+	 */
+	if (fs % 64)
+		return -EINVAL;
+
+	/* Set the divider between lrclk and bclk */
+	regmap_update_bits(priv->core->aiu, AIU_I2S_DAC_CFG,
+			   AIU_I2S_DAC_CFG_PAYLOAD_SIZE_MASK,
+			   AIU_I2S_DAC_CFG_AOCLK_64);
+
+	regmap_update_bits(priv->core->aiu, AIU_CODEC_DAC_LRCLK_CTRL,
+			   AIU_CODEC_DAC_LRCLK_CTRL_DIV_MASK,
+			   AIU_CODEC_DAC_LRCLK_CTRL_DIV(64));
+
+	/* Use CLK_MORE for the i2s divider */
+	regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL,
+			   AIU_CLK_CTRL_I2S_DIV_MASK,
+			   0);
+
+	regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL_MORE,
+			   AIU_CLK_CTRL_MORE_I2S_DIV_MASK,
+			   AIU_CLK_CTRL_MORE_I2S_DIV(fs / 64));
+
+	return 0;
+}
+
+static int __setup_desc(struct meson_i2s_dai *priv, unsigned int width,
+			unsigned int channels)
+{
+	u32 desc = 0;
+
+	switch (width) {
+	case 24:
+		/*
+		 * For some reason, 24 bits wide audio don't play well
+		 * if the 32 bits mode is not set
+		 */
+		desc |= (AIU_I2S_SOURCE_DESC_MODE_24BIT |
+			 AIU_I2S_SOURCE_DESC_MODE_32BIT);
+		break;
+	case 16:
+		break;
+
+	default:
+		return -EINVAL;
+	}
+
+	switch (channels) {
+	case 2: /* Nothing to do */
+		break;
+	case 8:
+		/* TODO: Still requires testing ... */
+		desc |= AIU_I2S_SOURCE_DESC_MODE_8CH;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	regmap_update_bits(priv->core->aiu, AIU_I2S_SOURCE_DESC,
+			   AIU_I2S_SOURCE_DESC_MODE_8CH |
+			   AIU_I2S_SOURCE_DESC_MODE_24BIT |
+			   AIU_I2S_SOURCE_DESC_MODE_32BIT,
+			   desc);
+
+	return 0;
+}
+
+static int meson_i2s_dai_hw_params(struct snd_pcm_substream *substream,
+				   struct snd_pcm_hw_params *params,
+				   struct snd_soc_dai *dai)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+	unsigned int width = params_width(params);
+	unsigned int channels = params_channels(params);
+	unsigned int rate = params_rate(params);
+	int ret;
+
+	ret = __setup_desc(priv, width, channels);
+	if (ret) {
+		dev_err(dai->dev, "Unable set to set i2s description\n");
+		return ret;
+	}
+
+	ret = __bclks_set_rate(priv, rate, width);
+	if (ret) {
+		dev_err(dai->dev, "Unable set to the i2s clock rates\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int meson_i2s_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+	u32 val;
+
+	if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBS_CFS)
+		return -EINVAL;
+
+	/* DAI output mode */
+	switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+		val = AIU_CLK_CTRL_ALRCLK_I2S;
+		break;
+	case SND_SOC_DAIFMT_LEFT_J:
+		val = AIU_CLK_CTRL_ALRCLK_LEFT_J;
+		break;
+	case SND_SOC_DAIFMT_RIGHT_J:
+		val = AIU_CLK_CTRL_ALRCLK_RIGHT_J;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL,
+			   AIU_CLK_CTRL_ALRCLK_SKEW_MASK,
+			   val);
+
+	/* DAI clock polarity */
+	switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+	case SND_SOC_DAIFMT_IB_IF:
+		/* Invert both clocks */
+		val = AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED |
+			AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED;
+		break;
+	case SND_SOC_DAIFMT_IB_NF:
+		/* Invert bit clock */
+		val = AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL |
+			AIU_CLK_CTRL_AOCLK_POLARITY_INVERTED;
+		break;
+	case SND_SOC_DAIFMT_NB_IF:
+		/* Invert frame clock */
+		val = AIU_CLK_CTRL_ALRCLK_POLARITY_INVERTED |
+			AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL;
+		break;
+	case SND_SOC_DAIFMT_NB_NF:
+		/* Normal clocks */
+		val = AIU_CLK_CTRL_ALRCLK_POLARITY_NORMAL |
+			AIU_CLK_CTRL_AOCLK_POLARITY_NORMAL;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	regmap_update_bits(priv->core->aiu, AIU_CLK_CTRL,
+			   AIU_CLK_CTRL_ALRCLK_POLARITY_MASK |
+			   AIU_CLK_CTRL_AOCLK_POLARITY_MASK,
+			   val);
+
+	switch (fmt & SND_SOC_DAIFMT_CLOCK_MASK) {
+	case SND_SOC_DAIFMT_CONT:
+		priv->bclks_idle = true;
+		break;
+	case SND_SOC_DAIFMT_GATED:
+		priv->bclks_idle = false;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int meson_i2s_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id,
+				    unsigned int freq, int dir)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+	int ret;
+
+	if (WARN_ON(clk_id != 0))
+		return -EINVAL;
+
+	if (dir == SND_SOC_CLOCK_IN)
+		return 0;
+
+	ret = clk_set_rate(priv->mclk, freq);
+	if (ret) {
+		dev_err(dai->dev, "Failed to set sysclk to %uHz", freq);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int meson_i2s_dai_startup(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+	int ret;
+
+	/* Power up the i2s fast domain - can't write the registers w/o it */
+	ret = clk_prepare_enable(priv->fast);
+	if (ret)
+		goto out_clk_fast;
+
+	/* Make sure nothing gets out of the DAI yet */
+	__hold(priv, true);
+
+	/* I2S encoder needs the mixer interface gate */
+	ret = clk_prepare_enable(priv->iface);
+	if (ret)
+		goto out_clk_iface;
+
+	/* Enable the i2s master clock */
+	ret = clk_prepare_enable(priv->mclk);
+	if (ret)
+		goto out_mclk;
+
+	/* Enable the bit clock gate */
+	ret = clk_prepare_enable(priv->bclks);
+	if (ret)
+		goto out_bclks;
+
+	/* Make sure the interface expect a memory layout we can work with */
+	regmap_update_bits(priv->core->aiu, AIU_I2S_SOURCE_DESC,
+			   AIU_I2S_SOURCE_DESC_MODE_SPLIT,
+			   AIU_I2S_SOURCE_DESC_MODE_SPLIT);
+
+	return 0;
+
+out_bclks:
+	clk_disable_unprepare(priv->mclk);
+out_mclk:
+	clk_disable_unprepare(priv->iface);
+out_clk_iface:
+	clk_disable_unprepare(priv->fast);
+out_clk_fast:
+	return ret;
+}
+
+static void meson_i2s_dai_shutdown(struct snd_pcm_substream *substream,
+				   struct snd_soc_dai *dai)
+{
+	struct meson_i2s_dai *priv = snd_soc_dai_get_drvdata(dai);
+
+	clk_disable_unprepare(priv->bclks);
+	clk_disable_unprepare(priv->mclk);
+	clk_disable_unprepare(priv->iface);
+	clk_disable_unprepare(priv->fast);
+}
+
+static const struct snd_soc_dai_ops meson_i2s_dai_ops = {
+	.startup    = meson_i2s_dai_startup,
+	.shutdown   = meson_i2s_dai_shutdown,
+	.trigger    = meson_i2s_dai_trigger,
+	.hw_params  = meson_i2s_dai_hw_params,
+	.set_fmt    = meson_i2s_dai_set_fmt,
+	.set_sysclk = meson_i2s_dai_set_sysclk,
+};
+
+static struct snd_soc_dai_driver meson_i2s_dai = {
+	.playback = {
+		.stream_name = "Playback",
+		.channels_min = 2,
+		.channels_max = 8,
+		.rates = SNDRV_PCM_RATE_8000_192000,
+		.formats = (SNDRV_PCM_FMTBIT_S16_LE |
+			    SNDRV_PCM_FMTBIT_S24_LE)
+	},
+	.ops = &meson_i2s_dai_ops,
+};
+
+static const struct snd_soc_component_driver meson_i2s_dai_component = {
+	.name	= DRV_NAME,
+};
+
+static int meson_i2s_dai_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct meson_i2s_dai *priv;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+	priv->core = dev_get_drvdata(dev->parent);
+
+	priv->fast = devm_clk_get(dev, "fast");
+	if (IS_ERR(priv->fast)) {
+		if (PTR_ERR(priv->fast) != -EPROBE_DEFER)
+			dev_err(dev, "Can't get the i2s fast domain clock\n");
+		return PTR_ERR(priv->fast);
+	}
+
+	priv->iface = devm_clk_get(dev, "iface");
+	if (IS_ERR(priv->iface)) {
+		if (PTR_ERR(priv->iface) != -EPROBE_DEFER)
+			dev_err(dev, "Can't get i2s dai clock gate\n");
+		return PTR_ERR(priv->iface);
+	}
+
+	priv->bclks = devm_clk_get(dev, "bclks");
+	if (IS_ERR(priv->bclks)) {
+		if (PTR_ERR(priv->bclks) != -EPROBE_DEFER)
+			dev_err(dev, "Can't get bit clocks gate\n");
+		return PTR_ERR(priv->bclks);
+	}
+
+	priv->mclk = devm_clk_get(dev, "mclk");
+	if (IS_ERR(priv->mclk)) {
+		if (PTR_ERR(priv->mclk) != -EPROBE_DEFER)
+			dev_err(dev, "failed to get the i2s master clock\n");
+		return PTR_ERR(priv->mclk);
+	}
+
+	return devm_snd_soc_register_component(dev, &meson_i2s_dai_component,
+					       &meson_i2s_dai, 1);
+}
+
+static const struct of_device_id meson_i2s_dai_of_match[] = {
+	{ .compatible = "amlogic,meson-i2s-dai", },
+	{ .compatible = "amlogic,meson-gxbb-i2s-dai", },
+	{ .compatible = "amlogic,meson-gxl-i2s-dai", },
+	{}
+};
+MODULE_DEVICE_TABLE(of, meson_i2s_dai_of_match);
+
+static struct platform_driver meson_i2s_dai_pdrv = {
+	.probe = meson_i2s_dai_probe,
+	.driver = {
+		.name = DRV_NAME,
+		.of_match_table = meson_i2s_dai_of_match,
+	},
+};
+module_platform_driver(meson_i2s_dai_pdrv);
+
+MODULE_DESCRIPTION("Meson i2s DAI ASoC Driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
